Czas na zajęcie się trochę bardziej skomplikowanym komponentem. Przetestujemy OrderOption oraz jego subkomponenty, np. OrderOptionDropdown. Zaczynamy standardowo – stwórz plik OrderOption.test.js w katalogu tego komponentu.
Podstawowe testy komponentu OrderOption
Pisanie testu zacznij od zaimportowania Reacta, funkcji shallow oraz komponentu OrderOption. Napisz też funkcję describe, a w niej test it sprawdzający, czy komponent się renderuje. Pamiętaj, że musisz przekazać temu komponentowi przynajmniej propsy type i name.
Test powinien dać wynik pozytywny – możemy więc przejść do drugiego testu. Tym razem sprawdzimy, czy przy braku podanego typu opcji komponent zachowa się poprawnie, czyli zwróci null. Osiągniemy to, porównując wyrenderowany komponent z pustym obiektem.
it('should return empty object if called without required props', () => {
const component = shallow(<OrderOption />);
expect(component).toEqual({});
});
Wreszcie, ostatni test sprawdzający sam komponent OrderOption – napisz samodzielnie test, który upewni się, że w tytule wyświetla się zawartość propsa name.
Schemat testowania subkomponentów
Wiemy, że OrderOption renderuje jeden ze swoich subkomponentów, a każdy z nich działa dość podobnie. Zgodnie z zasadą Don't repeat yourself, postaramy się uniknąć niepotrzebnego duplikowania kodu. Właśnie dlatego stworzymy pętlę, która pomoże nam testować każdy z subkomponentów.
Na końcu pliku dodaj następujący kod:
const optionTypes = {
dropdown: 'OrderOptionDropdown',
icons: 'OrderOptionIcons',
checkboxes: 'OrderOptionCheckboxes',
number: 'OrderOptionNumber',
text: 'OrderOptionText',
date: 'OrderOptionDate',
};
for(let type in optionTypes){
describe(`Component OrderOption with type=${type}`, () => {
it('passes dummy test', () => {
expect(1).toBe(1);
});
switch (type) {
case 'dropdown': {
break;
}
}
});
}
Mamy tutaj obiekt zawierający wszystkie typy opcji oraz nazwy odpowiadających im subkomponentów. Następnie w pętli iterujemy po nich, zapisując typ opcji w zmiennej type.
Możesz teraz uruchomić testy – mamy tylko jeden przykładowy test, który zawsze zwróci prawdę (bo sprawdza tylko, czy 1 jest równe 1). Zauważ jednak, że ten test uruchomi się dla każdego typu opcji, którego nazwa będzie podana w opisie grupy testów (podanym w funkcji describe).
Przygotowanie do testów
Wewnątrz pętli wykorzystujemy describe do stworzenia nowego pakietu testów, którego opis zawiera typ, po którym aktualnie iterujemy.
W describe za pomocą komentarzy wydzieliliśmy trzy sekcje:
test setup, którym zajmiemy się za chwilę,
common tests, w którym będziemy pisać testy dotyczące każdego subkomponentu,
type-specific tests, gdzie znajdziesz wyrażenie switch – w nim będziemy pisać testy dla konkretnych typów opcji.
Ta ostatnia sekcja może wydawać Ci się dziwna – po co używamy pętli, skoro potem i tak będziemy pisać osobne testy dla każdego typu? Sekret leży w sekcji test setup! Dodamy w niej operacje, które będą wykonywane przed każdym testem – dzięki temu nie będziemy musieli się powtarzać!
W sekcji test setup wstaw następujący kod:
let component;
beforeEach(() => {
component = shallow(
<OrderOption
type={type}
/>
);
});
Funkcja beforeEach wykona się przed uruchomieniem każdego z testów it – oznacza to, że każdy test będzie miał do dyspozycji świeżo wyrenderowany komponent OrderOption, i nie musimy używać funkcji shallow w każdym z testów!
Możemy od razu to sprawdzić, wpisując w naszym przykładowym teście o nazwie "passes dummy test" znany już Ci console.log:
console.log(component.debug());
Jeśli wszystko poszło dobrze, test wyświetli w terminalu kod JSX każdego z wyrenderowanych komponentów. Zwróć uwagę, że w każdym z nich znalazł się inny subkomponent!
Renderowanie subkomponentów
Do przetestowania np. OptionOrderDropdown nie wystarczy nam jednak informacja, że OptionOrder go wykorzystuje. Musimy wyrenderować również ten subkomponent. Chcemy jednak upewnić się, że testujemy tylko ten jeden subkomponent – jeśli OptionOrder zawiera jakiekolwiek inne (np. Col czy Icon), chcemy, aby pozostały jako kod JSX. Dlatego nie użyjemy mount (o którym wspominaliśmy wcześniej) zamiast shallow – lepiej będzie skorzystać z metody .dive!
Zmień kod w sekcji test setup na następujący:
let component;
let subcomponent;
let renderedSubcomponent;
beforeEach(() => {
component = shallow(
<OrderOption
type={type}
/>
);
subcomponent = component.find(optionTypes[type]);
renderedSubcomponent = subcomponent.dive();
});
Nie przejmuj się, że testy zaczną sypać błędami – wręcz możesz się z tego ucieszyć! Dzieje się tak, ponieważ metoda .dive wyrenderowała subkomponenty, i to one zgłaszają błędy, ponieważ nie otrzymały propsów wymaganych do poprawnego działania.
Zanim to naprawimy, zwróć uwagę, że musieliśmy najpierw w wyrenderowanym komponencie OrderOption znaleźć subkomponent za pomocą metody .find. Jako selektora użyliśmy w tym wypadku po prostu nazwy subkomponentu.
Dodanie mockowanych propsów
Angielskie słowo mock na stałe zagości już w Twoim słowniku. Używamy go w znaczeniu, które tłumaczy się jako atrapa. W testach jednostkowych będziemy używać wielu atrap i już to nie raz zrobiliśmy – za każdym razem, kiedy podawaliśmy komponentowi jakieś propsy. Nie podajemy mu wtedy prawdziwych danych, z których korzysta nasza aplikacja, tylko jakieś przykładowe, stanowiące właśnie atrapę.
Tym razem pójdziemy o krok dalej i przygotujemy sobie cały obiekt zawierający wiele propsów, które chcemy nadawać subkomponentom. Jest to mieszanka właściwości różnych opcji z pliku pricing.json. Wstaw ten kod przed pętlą for:
const mockProps = {
id: 'abc',
name: 'Lorem',
values: [
{id: 'aaa', icon: 'h-square', name: 'Lorem A', price: 0},
{id: 'xyz', icon: 'h-square', name: 'Lorem X', price: 100},
],
required: false,
currentValue: 'aaa',
price: '50%',
limits: {
min: 0,
max: 6,
},
};
const mockPropsForType = {
dropdown: {},
icons: {},
checkboxes: {currentValue: [mockProps.currentValue]},
number: {currentValue: 1},
text: {},
date: {},
};
const testValue = mockProps.values[1].id;
const testValueNumber = 3;
W pliku pricing.json możesz sprawdzić, że tylko niektóre opcje mają właściwość values, a inne limits. My złączyliśmy wszystkie możliwości w jednym obiekcie mockProps, aby ułatwić sobie testowanie. Subkomponenty i tak będą korzystać tylko z niektórych propsów, więc nie musimy się tym przejmować.
Następny obiekt, mockPropsForType, zawiera propsy istotne tylko dla konkretnego typu opcji. Na przykład, OrderOptionCheckboxes wymaga, aby currentValue było tablicą, a number – liczbą.
Na końcu mamy dwie stałe, testValue i testValueNumber – będziemy się starali, aby każdy subkomponent przyjął właśnie tę wartość. Innymi słowy, ta wartość to nasz cel, do którego dążymy. Zwróć uwagę, że testValue odwołuje się do id drugiego obiektu w mockProps.values, podczas gdy mockProps.currentValue jest równe id pierwszego obiektu. W ten sposób zasymulujemy sytuację, w której opcja ma już jakąś wartość, którą chcemy zmienić na inną (lub do której dodamy inną, w przypadku checkboxes).
Teraz musimy wykorzystać te atrapy propsów w funkcji beforeEach. Pod propsem type w tagu <OrderOption /> dodaj:
{...mockProps}
{...mockPropsForType[type]}
Po tej zmianie nasz test "passes dummy test" powinien ponownie działać dla każdego typu opcji. Możesz dodać do niego drugi console.log, tym razem wyświetlający subcomponent.debug() (czyli subkomponent, a nie komponent). Sprawdź wynik w terminalu, aby zobaczyć, że faktycznie subkomponenty zostały wyrenderowane.
Wspólne testy subkomponentów
Czas zmienić ten przykładowy test na nieco bardziej przydatny:
it(`renders ${optionTypes[type]}`, () => {
expect(subcomponent).toBeTruthy();
expect(subcomponent.length).toBe(1);
});
Podobnie jak wcześniej dla komponentu, tak teraz dla każdego subkomponentu sprawdzamy, czy w ogóle się renderuje. Gdyby np. nasza struktura kodu wymagała, aby każdy z nich zawierał Col, moglibyśmy również to sprawdzić w sekcji common tests.
W naszym przypadku nie ma takiej potrzeby, więc możemy przejść do ostatniej sekcji – type-specific tests.
Testy dla dropdownów
W miejscu komentarza /* tests for dropdown */ wstawimy dwa testy. Pierwszy z nich sprawdzi, czy ten subkomponent zawiera odpowiednie elementy, czyli <select> i <option>. Szybkie spojrzenie do pliku OrderOptionDropdown.js przypomni Ci, że jeśli props required jest fałszywy (a w naszych mockowanych propsach ma wartość false), zostanie dodany dodatkowy <option> z pustą wartością. Sprawdzimy więc zarówno obecność selecta, opcji z pustą wartością, jak i po jednej opcji dla każdego obiektu z mockProps.values.
it('contains select and options', () => {
const select = renderedSubcomponent.find('select');
expect(select.length).toBe(1);
const emptyOption = select.find('option[value=""]').length;
expect(emptyOption).toBe(1);
const options = select.find('option').not('[value=""]');
expect(options.length).toBe(mockProps.values.length);
expect(options.at(0).prop('value')).toBe(mockProps.values[0].id);
expect(options.at(1).prop('value')).toBe(mockProps.values[1].id);
});
Zwróć uwagę, że w stałej emptyOption używamy selektora atrybutu, a w stałej options – metody .not. W ten sposób możemy wybrać, które opcje chcemy zapisać w każdej z tych stałych.
Test interaktywności
Wspominaliśmy o dwóch testach – drugi z nich zajmie się sprawdzeniem interaktywności tego subkomponentu!
Przypomnijmy najpierw, jak działa jego interaktywność. Komponent OrderOption otrzymuje propsa setOrderOption. Jest to funkcja, która ma otrzymywać obiekt w formacie {idOpcji: wartośćOpcji}. Ten komponent przekazuje subkomponentowi w propsie setOptionValue inną funkcję:
value => setOrderOption({[id]: value})
Każdy z subkomponentów na swój sposób wykorzystuje tę funkcję, zawartą w propsie setOptionValue – może uruchamiać ją przy różnych eventach i przekazywać jej różne wartości. W przypadku OrderOptionDropdown będzie to event change, a wartością będzie właściwość value obiektu zapisanego w event.currentTarget.
Aby poprawnie sprawdzić działanie tego subkomponentu, musimy zasymulować event change oraz zawartość event.currentTarget, aby sprawdzić, czy wtedy subkomponent w reakcji na ten event wykona funkcję setOptionValue, która wykona setOrderOption.
Mockowanie funkcji
Chwileczkę – skąd mamy wiedzieć, czy wywołana została ta funkcja? Jak to sprawdzić? Komponent OrderOption otrzymuje ją jako propsa, więc możemy mu przekazać dowolną funkcję! Co więcej, Jest posiada wbudowany mechanizm do mockowania funkcji, czyli budowania atrapy, która pozwoli nam sprawdzić, czy ta funkcja była wykonana, ile razy, z jakimi argumentami, etc.
Znajdź ten fragment kodu:
let renderedSubcomponent;
beforeEach(() => {
component = shallow(
<OrderOption
type={type}
{...mockProps}
{...mockPropsForType[type]}
/>
);
Zmień go, dodając 3 linie kodu, które oznaczyliśmy komentarzami poniżej:
let renderedSubcomponent;
let mockSetOrderOption;
beforeEach(() => {
mockSetOrderOption = jest.fn();
component = shallow(
<OrderOption
type={type}
setOrderOption={mockSetOrderOption}
{...mockProps}
{...mockPropsForType[type]}
/>
);
Pierwszą i ostatnią na pewno zrozumiesz, ale najciekawsza jest druga – wyrażenie jest.fn() to właśnie sposób na stworzenie atrapy funkcji! Za chwilę zobaczysz, jak będziemy ją wykorzystywać!
Symulowanie eventu i wykorzystanie atrapy funkcji
Dodaj kolejny test dla dropdowna, wewnątrz wyrażenia switch:
it('should run setOrderOption function on change', () => {
renderedSubcomponent.find('select').simulate('change', {currentTarget: {value: testValue}});
expect(mockSetOrderOption).toBeCalledTimes(1);
expect(mockSetOrderOption).toBeCalledWith({ [mockProps.id]: testValue });
});
Mamy tutaj trochę nowości, więc omówmy je po kolei. Po znalezieniu selecta wykonujemy na nim metodę .simulate, która przyjmuje jeden lub dwa argumenty. Pierwszym z nich jest rodzaj eventu, jaki ma zostać zasymulowany – w tym wypadku event change. Drugi argument to wartość przekazywana handlerowi tego eventu. Jak zwykle, handler eventu spodziewa się, że otrzyma obiekt event, ale nie musimy mockować całej jego zawartości – ten handler korzysta tylko z właściwości currentTarget, z której pobiera value. Dlatego właśnie jako drugi argument podaliśmy taką atrapę obiektu event.
Przechodząc do kolejnej linii kodu, sprawdzamy, czy ta funkcja została wykonana dokładnie jeden raz. Natomiast w ostatniej linii kodu sprawdzamy, czy została wywołana z poprawnymi argumentami.
Zmienna jako klucz w obiekcie
Warto przypomnieć, co oznaczają nawiasy kwadratowe w powyższym wyrażeniu. Przeanalizuj ten przykład:
const keyName = 'car-rental';
const options = {
[keyName]: 'SUV',
};
console.log(options);
W komentarzu przedstawiliśmy wynik, który zostałby wyświetlony w konsoli za pomocą console.log. Nawiasy kwadratowe pozwoliły nam wykorzystać wartość zmiennej jako klucz w obiekcie. Bez tego zapisu musielibyśmy zrobić to nieco naokoło:
const keyName = 'car-rental';
const options = {};
options[keyName] = 'SUV';
console.log(options);
W obu przypadkach rezultat będzie ten sam, ale to ten pierwszy zapis będzie dla nas najwygodniejszy – nie wymaga on zapisania zmiennej, czyli moglibyśmy z tym samym efektem napisać:
const keyName = 'car-rental';
console.log({
[keyName]: 'SUV',
});
Sprawdzając dokumentację Jesta nie zdziw się, że wykorzystane przez nas metody widnieją pod nazwami .toHaveBeenCalledTimes i .toHaveBeenCalledWith – w naszym kodzie użyliśmy ich aliasów, ponieważ są krótsze i bardziej czytelne.
Podsumowanie testów interakcji
Ten submoduł mógł być wyzwaniem dla Ciebie, ponieważ wykorzystuje nieco skomplikowany algorytm testowania wielu subkomponentów. Była to dla Ciebie okazja do treningu swojego rozumienia algorytmicznego. ;)
Jednak całe zagadnienie testu interaktywności zawarliśmy w powyższym rozdziale – wystarczyło stworzyć atrapę funkcji za pomocą jest.fn(), zasymulować event za pomocą .simulate (podając obiekt oczekiwany przez handler eventu), oraz sprawdzić, co przechwyciła atrapa funkcji za pomocą metod .toBeCalledTimes i .toBeCalledWith.
I to jest klucz do testów jednostkowych sprawdzających działanie interakcji – symulujemy event i sprawdzamy, czy wykonała się atrapa funkcji, podana w propsach.